/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package simhya.simengine.hybrid;

import java.util.ArrayList;
import org.apache.commons.math.ode.FirstOrderDifferentialEquations;
import simhya.model.flat.FlatModel;
import simhya.model.store.Function;
import simhya.model.store.Predicate;
import simhya.model.transition.*;
import java.util.HashMap;

import simhya.utils.RandomGenerator;
import simhya.simengine.GuardChecker;
import cern.jet.random.engine.RandomEngine;

/**
 *
 * @author luca
 */
public abstract class HybridOdeFunction implements 
                                        FirstOrderDifferentialEquations, GuardChecker {
    FlatModel model;
    int[] transitionToVariable;

    int dimension;
    int stateVariableDimension;
    int eventVariableDimension;
    int numberOfContinuousTransitions;
    int numberOfTransitions;
    int numberOfStochasticTransitions;
    int numberOfHybridTransitions;

    

    Function [] rate;
    Predicate [] guard;
    Stoichiometry[][] stoich;
    boolean [] hasNonTrivialFlow;
    int[] continuousTransitions;
    int[] stochasticTransitions;
    int[] hybridTransitions;

    boolean[] continuityStatus;
    boolean[] isTrulyHybridTransition;

    boolean useCache = false;

    /**
     * constructs an instance of an hybrid ode function
     * @param model a flat model
     * @param allSwitchable a flag, if true, all continuously approximable
     * transitions may be switchable.
     */
    public HybridOdeFunction(FlatModel model, boolean allSwitchable) {
        this.model = model;
        this.numberOfTransitions = model.getNumberOfTransitions();
        this.transitionToVariable = new int[numberOfTransitions];
        if (!allSwitchable)
            this.initializeModelSwitchable();
        else
            this.initializeAllSwitchable();
    }


    private void initializeAllSwitchable() {
        this.numberOfHybridTransitions = model.getNumberOfContinuousTransitions(true);
        this.numberOfStochasticTransitions = this.numberOfTransitions -
                this.numberOfHybridTransitions - model.getNumberOfInstantaneousTransitions() -
                model.getNumberOfTimedTransitions();
        this.numberOfContinuousTransitions = 0;
        this.hybridTransitions = model.getListOfContinuousTransitionID(true);
        this.continuousTransitions = new int[0];
        this.stochasticTransitions = new int[numberOfStochasticTransitions];
        int code = 0;
        for (Integer j : model.getListOfStochasticTransitionID(false)) {
            if (!simhya.utils.ArrayUtils.contains(hybridTransitions, j.intValue()))
                stochasticTransitions[code++] = j;
        }
        this.continuityStatus = new boolean[this.numberOfTransitions];
        java.util.Arrays.fill(continuityStatus, false);
        this.isTrulyHybridTransition = new boolean[this.numberOfTransitions];
        java.util.Arrays.fill(isTrulyHybridTransition, false);
        //here we set the continuity status only provisionally.
        //global switching conditions has to be evaluated and the continuity status
        //changed accordingly.
        for (Integer j : model.getListOfHybridTransitionID()) {
            continuityStatus[j] = model.getTransition(j).evaluateContinuityStatus();
            this.isTrulyHybridTransition[j] = true;
        }
        for (Integer j : model.getListOfContinuousTransitionID(false))
            continuityStatus[j] = true;
        this.stateVariableDimension = model.getStore().getNumberOfVariables();
        this.eventVariableDimension = numberOfStochasticTransitions + numberOfHybridTransitions;
        dimension = this.stateVariableDimension + this.eventVariableDimension;
        rate = model.getTransitionRates();
        guard = model.getTransitionGuards();
        stoich = new Stoichiometry[this.numberOfTransitions][];
        hasNonTrivialFlow = new boolean[dimension];
        java.util.Arrays.fill(hasNonTrivialFlow, false);
        AtomicReset[][] resets = model.getTransitionResets();
        //initialize continuous treatment of hybrid transitions
        for (Integer j : this.hybridTransitions) {
            stoich[j] = new Stoichiometry[resets[j].length];
            for (int i=0;i<resets[j].length;i++) {
                int k = resets[j][i].getUpdatedVariables().get(0);
                stoich[j][i] = new Stoichiometry(k, resets[j][i].getConstantIncrement());
                hasNonTrivialFlow[k] = true;
            }
        }
        this.model.getStore().getVariablesReference().setContinuousEvolutionReference(hasNonTrivialFlow);
        //initialize stochastic variables, but NOT their initial values.
        //their values are to be initialized by the integrator
        int id = this.stateVariableDimension;
        for (Integer j : this.stochasticTransitions) {
            this.transitionToVariable[j] = id;
            id++;
        }
        for (Integer j : this.hybridTransitions) {
            this.transitionToVariable[j] = id;
            id++;
        }
    }

    private void initializeModelSwitchable() {
        this.numberOfHybridTransitions = model.getNumberOfHybridTransitions();
        this.numberOfStochasticTransitions = model.getNumberOfStochasticTransitions(false);
        this.numberOfContinuousTransitions = model.getNumberOfContinuousTransitions(false);
        this.stochasticTransitions = model.getListOfStochasticTransitionID(false);
        this.hybridTransitions = model.getListOfHybridTransitionID();
        this.continuousTransitions = model.getListOfContinuousTransitionID(false);

        //debug
//        System.out.println("continuous transitions");
//        for (Integer j : this.continuousTransitions)
//            System.out.print(model.getTransition(j).getEvent() + ",  ");
//        System.out.println();
//        System.out.println("stochastic transitions");
//        for (Integer j : this.stochasticTransitions)
//            System.out.print(model.getTransition(j).getEvent() + ",  ");
//        System.out.println();
//        System.out.println("hybrid transitions");
//        for (Integer j : this.hybridTransitions)
//            System.out.print(model.getTransition(j).getEvent() + ",  ");
//        System.out.println();

        this.continuityStatus = new boolean[this.numberOfTransitions];
        java.util.Arrays.fill(continuityStatus, false);
        this.isTrulyHybridTransition = new boolean[this.numberOfTransitions];
        java.util.Arrays.fill(isTrulyHybridTransition, false);
        for (Integer j : this.hybridTransitions) {
            continuityStatus[j] = model.getTransition(j).evaluateContinuityStatus();
            this.isTrulyHybridTransition[j] = true;
        }
        this.stateVariableDimension = model.getStore().getNumberOfVariables();
        this.eventVariableDimension = numberOfStochasticTransitions + numberOfHybridTransitions;
        dimension = this.stateVariableDimension + this.eventVariableDimension;
        rate = model.getTransitionRates();
        guard = model.getTransitionGuards();
        stoich = new Stoichiometry[this.numberOfTransitions][];
        hasNonTrivialFlow = new boolean[dimension];
        java.util.Arrays.fill(hasNonTrivialFlow, false);
        AtomicReset[][] resets = model.getTransitionResets();
        //initialize continuous flows
        for (Integer j : this.continuousTransitions) {
            stoich[j] = new Stoichiometry[resets[j].length];
            for (int i=0;i<resets[j].length;i++) {
                int k = resets[j][i].getUpdatedVariables().get(0);
                stoich[j][i] = new Stoichiometry(k,resets[j][i].getConstantIncrement());
                hasNonTrivialFlow[k] = true;
            }
        }
        //initialize continuous treatment of hybrid transitions
        for (Integer j : this.hybridTransitions) {
            stoich[j] = new Stoichiometry[resets[j].length];
            for (int i=0;i<resets[j].length;i++) {
                int k = resets[j][i].getUpdatedVariables().get(0);
                stoich[j][i] = new Stoichiometry(k, resets[j][i].getConstantIncrement());
                hasNonTrivialFlow[k] = true;
            }
        }
        this.model.getStore().getVariablesReference().setContinuousEvolutionReference(hasNonTrivialFlow);
        //initialize stochastic variables, but NOT their initial values.
        //their values are to be initialized by the integrator
        int id = this.stateVariableDimension;
        for (Integer j : this.stochasticTransitions) {
            this.transitionToVariable[j] = id;
            id++;
        }
        for (Integer j : this.hybridTransitions) {
            this.transitionToVariable[j] = id;
            id++;
        }
    }

    public void setUseCache(boolean useCache) {
        this.useCache = useCache;
    }

    
    /**
     * returns the id of the variable associated to a specific stochasic event
     * @param transitionID
     * @return
     */
    public int getVariableOfStochasticEvent(int transitionID) {
        return this.transitionToVariable[transitionID];
    }

    
    public boolean[] getContinuityStatus() {
        return this.continuityStatus;
    }

    public boolean getContinuityStatus(int id) {
        return this.continuityStatus[id];
    }

    public void setContinuityStatus(boolean[] status) {
        if (status.length != this.continuityStatus.length)
            throw new IllegalArgumentException("Status vector has wrong length");
        this.continuityStatus = status;
    }

    public void setContinuityStatus(int id, boolean status) {
        if (id < 0 && id >= this.numberOfTransitions)
            throw new IndexOutOfBoundsException("Transition " + id + " does not exist");
        this.continuityStatus[id] = status;
    }


    public int getDimension() {
        return this.dimension;
    }

    public double[] getVariables(FlatModel model, RandomEngine rand) {
        double [] variables = new double[this.dimension];
        for (int i=0;i<this.stateVariableDimension;i++)
            variables[i] = model.getStore().getVariablesReference().getValue(i);
        for (int i=this.stateVariableDimension;i<this.dimension;i++)
            variables[i] = RandomGenerator.expDist(1, rand);
        return variables;
    }

    public boolean evolvesContinuously(Predicate p) {
        boolean answer = false;
        for (Integer j : p.getVariableList()) {
            answer = answer || hasNonTrivialFlow[j];
        }
        return answer;
    }

    public boolean evolvesContinuously(Function f) {
        boolean answer = false;
        for (Integer j : f.getVariableList()) {
            answer = answer || hasNonTrivialFlow[j];
        }
        return answer;
    }

}


class Stoichiometry {
    public int variable;
    public double coefficient;

    public Stoichiometry(int variable, double coefficient) {
        this.variable = variable;
        this.coefficient = coefficient;
    }
}